package au.com.langdale.gwt.client.ui;

import au.com.langdale.gwt.client.binding.BindingManager.Binding;
import au.com.langdale.gwt.client.model.ModelContainer;
import au.com.langdale.gwt.client.model.ModelInfo;
import au.com.langdale.gwt.client.model.ModelListener;
import au.com.langdale.gwt.client.model.Property;
import au.com.langdale.gwt.client.remote.EditManager;

import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FocusListener;
import com.google.gwt.user.client.ui.FocusWidget;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.SourcesTableEvents;
import com.google.gwt.user.client.ui.TableListener;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
import com.google.gwt.user.client.ui.HTMLTable.ColumnFormatter;
import com.google.gwt.user.client.ui.HTMLTable.RowFormatter;

/**
 * A data-bound table view with editing functions.<p>  
 * 
 * The view displays the contents of a ModelContainer instance, 
 * such as RemoteList, in a grid.<p>  
 * 
 * - Each grid row displays one one model from the ModelContainer. 
 * - Each column of the grid displays one property of the models.<p>  
 * 
 * A ModelInfo instance determines which properties are displayed,
 * their order, and their column headings.  The ModelInfo can be
 * generated from a database table or custom contructed.<p> 
 * 
 * The view also provides editing operations if it is given an
 * EditManager instance.   When the EditManager is present, clicking
 * on a grid cell will display a widget that permits the user to 
 * edit the cell value.  Clicking on a row (in the margin) selected
 * the row in the EditManager, enabling it to be deleted.<p> 
 * 
 * TODO: TableView has no provision for insertion.
 */
public class TableView extends Composite implements ModelListener {

	/**
	 * The TableController handles editing.  There is one TableController
	 * per TableView and it receives click events from the grid and
	 * focus events from editing widgets.  It interprets these
	 * for the EditManager. 
	 */
	public class TableController implements TableListener, FocusListener {
		private boolean editing;
		private int editingRow, editingCol;
		private Binding binding;

		private TableController() {
			grid.addTableListener(this);
		}

		private boolean isValidCol(int col) {
			if(modelInfo == null )
				return false;

			Property[] keys = modelInfo.getKeyProperties();
			Property[] props = modelInfo.getProperties();

			return	col >= MARGIN && 
					col < keys.length +	props.length + MARGIN;
		}

		private boolean isValidRow(int row) {
			return 	row >= HEADING && row < source.size() + HEADING;
		}

		public void onCellClicked(SourcesTableEvents source, int row, int col) {
			if( manager == null || source == null)
				return;
			
			System.out.println("Clicked " + row + ", " + col);
			if( isValidRow(row)) {
				selectRow(row);
				if( isValidCol(col))
					selectCell(row, col);
			}
		}

		public void selectRow(int row) {
			manager.setSelected(source.get(row-HEADING));
		}

		public void selectCell(final int row, final int col) {
			if( editing && row == editingRow && col == editingCol)
				return; // already editing this one

			deselectCell();
			editingRow = row;
			editingCol = col;
			editing = true;

			System.out.println("Selecting " + editingRow +", " + editingCol);
			
			Object model = source.get(row-HEADING);

			Property[] keys = modelInfo.getKeyProperties();
			Property[] props = modelInfo.getProperties();

			Property prop;
			if( col >= keys.length + MARGIN )
				prop = props[col - keys.length - MARGIN];
			else
				prop = keys[col - MARGIN];

			binding = manager.findBinding(model, prop);
			if( binding == null )
				binding = manager.addBinding(model, prop);
			
			Widget widget = binding.getWidget();
			grid.setWidget(row, col, widget);
			
			// focus handling if required by the widget
			if( widget instanceof FocusWidget) {
				FocusWidget focusWidget = (FocusWidget)widget;
				focusWidget.setFocus(true);
				focusWidget.addFocusListener(this);
			}
		}

		public void deselectCell() {
			if( ! editing )
				return;
			editing = false;
			
			System.out.println("Deselecting " + editingRow +", " +editingCol);
			
			Widget widget = binding.getWidget();
			if( widget instanceof FocusWidget) {
				FocusWidget focusWidget = (FocusWidget)widget;
				focusWidget.removeFocusListener(TableController.this);
			}
			
			initCell(editingRow, editingCol, binding.getModel(), binding.getProperty(), 
					binding.isPending() ? "updated": "cell"); // cease editing
		}

		public void onFocus(Widget sender) {
			// noop
		}

		public void onLostFocus(Widget sender) {
			
			// can't alter listeners in this handler
			DeferredCommand.add( new Command() {
				public void execute() {
					deselectCell();
				}
			});
		}

		public void reset() {
			if( manager == null )
				return;
			manager.clearUpdates();
			manager.removeBindings();
			manager.clearSelection();
		}
	}

	private final Grid grid = new Grid();

	private ModelInfo modelInfo;

	private ModelContainer source;
	
	private TableController controller = new TableController();

	private EditManager manager;
	
	private static final int HEADING = 1;
	
	private static final int MARGIN = 1;

	public TableView() {
		initWidget(grid);
		grid.setStyleName("table");
		grid.resize(HEADING, MARGIN);
		setStyleName("DynaTable-DynaTableWidget");
	}

	public EditManager getEditManager() {
		return manager;
	}

	public ModelInfo getModelInfo() {
		return modelInfo;
	}

	public ModelContainer getSource() {
		return source;
	}

	public void setSource(ModelContainer sourceModel) {
		if( source != null )
			source.removeModelListener(this);
		source = sourceModel;
		if( getRowCount() > 0 )
			source.setQuota(getRowCount());
		source.addModelListener(this);
	}

	public int getRowCount() {
		return grid.getRowCount() - HEADING;
	}
	
	private void initHeadings(int col, Property[] props) {
		CellFormatter formatter = grid.getCellFormatter();
		ColumnFormatter columns = grid.getColumnFormatter();

		for (int i = 0; i < props.length; i++) {
			String label = props[i].toString();
			grid.setText(0, col + i + MARGIN, label);
			formatter.setStyleName(0, col + i + MARGIN, "header");
			formatter.addStyleName(0, col + i + MARGIN, props[i].getStyleName());
			columns.setWidth(col + i + MARGIN, props[i].getLength() + "em");
		}
	}

	private void initMargin() {
		CellFormatter formatter = grid.getCellFormatter();
		RowFormatter row_format = grid.getRowFormatter();
		
		for( int i = HEADING; i < grid.getRowCount(); ++i) {
			formatter.setStyleName(i, 0, "margin");
			row_format.setStyleName(i, (i - HEADING) % 2 == 0 ? "even": "odd");
		}
	}

	private void initRow(int row, int col, Object model, Property[] props, String style) {
		for (int i = 0; i < props.length; i++) {
			initCell(row, col + i + MARGIN, model, props[i], style);
		}

	}

	private void initCell(int row, int col, Object model, Property prop, String style) {
		grid.setText(row, col, prop.get(model));
		grid.getCellFormatter().setStyleName(row, col, style);
		grid.getCellFormatter().addStyleName(row, col, prop.getStyleName());
	}

	public void refresh() {
		if( modelInfo == null || source == null || getRowCount() == 0)
			return;

		controller.reset();

		int destRowCount = grid.getRowCount();
		int destColCount = grid.getCellCount(0) - MARGIN;
		int srcRowCount = source.size();

		Property[] keys = modelInfo.getKeyProperties();
		Property[] props = modelInfo.getProperties();

		int srcRowIndex = 0;
		int destRowIndex = HEADING; // skip title row

		// reset the style names
		initMargin();
		
		// Fill rows
		for (; srcRowIndex < srcRowCount && destRowIndex < destRowCount; ++srcRowIndex, ++destRowIndex) {
			Object model = source.get(srcRowIndex);
			initRow(destRowIndex, 0, model, keys, "key");
			initRow(destRowIndex, keys.length, model, props, "value");
		}

		// Clear remaining table rows.
		for (; destRowIndex < destRowCount; ++destRowIndex) {
			for (int destColIndex = 0; destColIndex < destColCount; ++destColIndex) {
				grid.clearCell(destRowIndex, destColIndex + MARGIN);
			}
		}
	}

	public void setModelInfo(ModelInfo info) {
		modelInfo = info;
		Property[] keys = info.getKeyProperties();
		Property[] props = info.getProperties();
		grid.resizeColumns(keys.length + props.length + MARGIN);
		grid.getCellFormatter().setStyleName(0, 0, "header");
		initHeadings(0, keys);
		initHeadings(keys.length, props);
		refresh();
	}

	public void setRowCount(int rowCount) {
		grid.resizeRows(rowCount + HEADING);
		if( source != null)
			source.setQuota(rowCount);
		refresh();
	}

	public void setEditManager(EditManager editManager) {
		if( manager != null )
			manager.removeModelListener(this);
		
		manager = editManager;
		manager.addModelListener(this);
	}

	public void onChange(Object container) {
		// change in status of model not displayed in this view
	}

	public void onContentsChange(Object container) {
		refresh();		
	}

	public void onModelChange(Object model) {
		// we expect this event from EditManager
		int row = source.indexOf(model);
		if( row == -1)
			return;
		row += HEADING;
		
		RowFormatter formatter = grid.getRowFormatter();
		if( model == manager.getSelected())
			formatter.addStyleName(row, "selected");
		else
			formatter.removeStyleName(row, "selected");
		if( manager.isDeleted(model)) {
			formatter.addStyleName(row, "deleted");
		}
	}

	public void onCommit(Object container) {
		// this is far from ideal because there might be 
		// more than one view or no views, 
		// but we will trigger a refresh
		source.refresh();
	}
}
